import pygame, sys, time, random
from pygame.locals import *

# this paragraph is just your standard pygame stuff
pygame.init()
screen_width = 640
screen_height = 480
screen = pygame.display.set_mode((screen_width, screen_height), RESIZABLE, 32)
pygame.display.set_caption("Space Invaders")
screen.fill((0, 0, 0))

"""#########################################################
   #                   BEGIN CLASSES                       #
   #########################################################
"""

class Player:
	def __init__(self):
		# Standard set up of position and sprite
		self.x = screen_width/2
		self.y = screen_height - 40
		self.rect = pygame.Rect(self.x,self.y,50,20)
		
	def move(self, direction):
		# Takes input and moves x cordinate
		if direction == "left":
			self.rect.move_ip(-2, 0)
		elif direction == "right":
			self.rect.move_ip(2, 0)
	
	def draw(self):
		pygame.draw.rect( screen, (0,255,0), self.rect )
		
	def get_rect(self):
		return self.rect
		
class Bullet:
	def __init__(self, x):
		# Bullet's initial y position is the same as the player's
		self.y = screen_height - 40
		self.rect = pygame.Rect(x, self.y, 2, 10)
		self.in_screen = True
	
	def update(self):
		# Keeps going up
		self.rect.move_ip(0, -4)
		pygame.draw.rect( screen, (255,255,255), self.rect )
		
		# If out of screen, report it and it will be deleted
		if self.rect.bottom < 0:
			self.in_screen = False
	def get_rect(self):
		return self.rect
			
class Alien:
	def __init__(self, x, y):
		self.rect = pygame.Rect(x, y, 40, 15)
		self.direction = "right"
	
	def update(self):
		if self.direction == "right":
			self.rect.move_ip(1,0)
		elif self.direction == "left":
			self.rect.move_ip(-1,0)
		
		pygame.draw.rect( screen, (255,255,255), self.rect )
		
	def switch_direction(self,dir):
		self.direction = dir
			
	def move_down_row(self):
		self.rect.move_ip(0, 15)
		
	def get_rect(self):
		return self.rect
		
class Alien_Bullet:
	def __init__(self, x, y):
		self.y = y
		self.x = x
		self.size = 5
		self.rect = pygame.draw.circle( screen, (255,255,255), (self.x,self.y), self.size )
		self.in_screen = True
	
	def update(self):
		# Blinking bullets of doom.
		self.y += 2
		self.size = 5 if self.size == 10 else 10
		self.rect = pygame.draw.circle( screen, (255,255,255), (self.x,self.y), self.size )
		
		if self.rect.bottom < 0:
			self.in_screen = False
		
	def get_rect(self):
		return self.rect
		
"""#########################################################
   #                   BEGIN FUNTIONS                      #
   #########################################################
"""		

def display_score(score):
	# Create a font
	font = pygame.font.Font(None, 30)
	
	# Render the text
	text = font.render(str(score), True, (255, 255, 255), (0,0,0))
	
	# Create a rectangle
	textRect = text.get_rect()
	
	# Position the rectangle
	textRect.top = 5
	textRect.right = screen_width - 5
	
	# Blit the text
	screen.blit(text, textRect)
		
def setup_aliens():
	# Uses magic to setup rows and columns of aliens
	aliens = []
	for i in range(1, 8):
		row = []
		x = 60 * i
		for j in range(1, 8):
			y = 30 * j
			row.append( Alien(x, y) )
		aliens.append(row)
	return aliens
	
def get_alien_boundaries(side, aliens):
	# Checks the x value of left-most or right-most columnof aliens and returns value
	if side == "left":
		lowest = 9001
		for row in aliens:
			if row and row[0].get_rect().left < lowest:
				lowest = row[0].get_rect().left
		return lowest
		
	elif side == "right":
		highest = -2
		for row in aliens:
			if row and row[-1].get_rect().right > highest:
				highest = row[-1].get_rect().right
		return highest
		
def check_if_aliens_dead(aliens):
	for a in aliens:
		if a != []:
			return False
	return True
		
def check_if_gameover(aliens, player, alien_bullets):
	# This checks to see if the bottom row is as low as the player
	for a in aliens:
		if a and a[-1].get_rect().bottom >= player.get_rect().top:
			return True
			
	if player.get_rect().collidelist( [ab.get_rect() for ab in alien_bullets ] ) != -1:
		return True
		
	return False
	
def display_highscores():
	# Display high scores and asks to start game.
	screen.fill((0, 0, 0))
	f = open("scores", "r").readlines()		
	s = 0
	players = len(f)*30+75 if len(f)*30+75 <= 330 else 350
	for i in  range(75, players, 30):
		s += 1
		# Create a font
		font = pygame.font.Font(None, 30)
		
		# Render the text
		text = font.render(str(s) + '. ' + f[s-1].replace('\n',''), True, (255, 255, 255), (0,0,0))
		
		# Create a rectangle
		textRect = text.get_rect()
		
		# Position the rectangle
		textRect.center = (screen_width/2, i)
		textRect.left = screen_width/3+50
		
		# Blit the text
		screen.blit(text, textRect)
	
	font = pygame.font.Font(None, 50)
	text = font.render("SPACE INVADERS", True, (255, 255, 255), (0,0,0))
	textRect = text.get_rect()
	textRect.top = 10
	textRect.left = screen_width/3-50
	screen.blit(text, textRect)
	
	text = font.render("PRESS SPACE TO PLAY", True, (0, 255, 0), (0,0,0))	
	textRect.bottom = screen_height-10
	textRect.left = screen_width/3-50
	screen.blit(text, textRect)
	
def get_highscore_placement(score):
	# Reads the highscore board in order. Checks if you beat a certain score
	# to figure out where you place. Return that placement
	f = open("scores", 'r').readlines()
	place = 1
	for l in f:
		s = int(l.replace("\n", '').split()[1])
		if score > s:
			return place
		place += 1
	return False
	
def cycle_letter(letter, direction):
	if direction == "up":
		if letter == "Z":
			return 'A'
		return chr( ord(letter)+1 )
	if direction == "down":
		if letter == "A":
			return 'Z'
		return chr( ord(letter)-1 )
	
def add_to_high_score(score, placement):
	# Takes the placement score, asks for a name through a simple interface,
	# then writes it to the file in that location.
	
	screen.fill((0,0,0))
	# Draw score and words
	text = pygame.font.Font(None, 50).render("SPACE INVADERS", True, (255, 255, 255), (0,0,0))
	textRect = text.get_rect()
	textRect.top = 10
	textRect.left = screen_width/3-50
	screen.blit(text, textRect)

	text = pygame.font.Font(None, 50).render(str(score), True, (255, 255, 255), (0,0,0))
	textRect = text.get_rect()
	textRect.top = screen_height * (2/3)
	textRect.left = screen_width/2-25
	screen.blit(text, textRect)
	
	# set selection to first letter. Arrow left, selection goes left-1, right +1. min 0, max 2
	selection = 0
	arrow_pos = [200, 300, 400]
	name = ['A', 'A', 'A']
	
	while True:
		pygame.display.update()
		screen.fill((0,0,0))
		text = pygame.font.Font(None, 50).render("NEW HIGHSCORE!", True, (255, 255, 255), (0,0,0))
		textRect = text.get_rect()
		textRect.top = 10
		textRect.left = screen_width/3-50
		screen.blit(text, textRect)
	
		text = pygame.font.Font(None, 50).render(str(score), True, (255, 255, 255), (0,0,0))
		textRect = text.get_rect()
		textRect.top = 55
		textRect.left = screen_width/2-25
		screen.blit(text, textRect)
		
		# Check for quit and also for key movements. Adjust arrows or letters accordingly
		for event in pygame.event.get(): 
			if event.type == QUIT: 
				pygame.quit()
				sys.exit()
			if event.type == KEYDOWN:
				if event.key == K_LEFT and selection > 0:
					selection -= 1
				elif event.key == K_RIGHT and selection < 2:
					selection += 1
				elif event.key == K_UP:
					name[selection] = cycle_letter(name[selection], 'up')
				elif event.key == K_DOWN:
					name[selection] = cycle_letter(name[selection], 'down')
					
		# Draw arrows to selected area
		pygame.draw.polygon( screen, (255,255,255), [(arrow_pos[selection], 200), (arrow_pos[selection]+20, 200), (arrow_pos[selection]+10, 190)] )
		pygame.draw.polygon( screen, (255,255,255), [(arrow_pos[selection], 270), (arrow_pos[selection]+20, 270), (arrow_pos[selection]+10, 280)] )
		
		# Draw name. Yes, this does use the arrow positions as a reference. So sue me.
		for i in range(0, 3):
			text = pygame.font.Font(None, 50).render(name[i], True, (255, 255, 255), (0,0,0))
			textRect = text.get_rect()
			textRect.top = 220
			textRect.left = arrow_pos[i]
			screen.blit(text, textRect)
		
		# Draw "Press space to save" if position is on last character
		if selection == 2:
			text = pygame.font.Font(None, 50).render("Press Space To Save", True, (255, 255, 255), (0,0,0))
			textRect = text.get_rect()
			textRect.top = screen_height * (2/3)
			textRect.left = screen_width/4-25
			screen.blit(text, textRect)
		
			# Put name together with score and insert to old_f[placement]. Write to file.
			keys_pressed = pygame.key.get_pressed()
			if keys_pressed[K_SPACE]:
				old_f = open("scores", 'r').readlines()
				new_f = open("scores", 'w')
				old_f.insert(placement-1, "".join(name) + ' ' + str(score) + '\n')
				for l in old_f:
					new_f.write(l)
				new_f.close()
				break
		
def game():
	""" 
	The single most important function. This is the whole game
	"""
	
	player = Player()
	bullet = 0
	aliens = setup_aliens()
	score = 0
	alien_bullets = []
	while True:
		# Upate screen
		pygame.display.update()
		# Draw background
		screen.fill((0, 0, 0))
		# Check if trying to close window
		for event in pygame.event.get(): 
			if event.type == QUIT: 
				pygame.quit()
				sys.exit()
		
		# Get player input and move player
		pressed_keys = pygame.key.get_pressed()
		if pressed_keys[K_RIGHT] and player.get_rect().right < screen_width:
			player.move("right")
		elif pressed_keys[K_LEFT] and player.get_rect().left > 0:
			player.move("left") 
			
		# Check fire input and make new bullet if one doesn't exist
		if pressed_keys[K_SPACE] and not bullet:
			# Starts in the middle of the player
			bullet = Bullet( player.get_rect().centerx )
			
		player.draw()
		
		display_score(score)
		
		# Checks if the bullet is there, updates it, kills it if not in screen
		if bullet:
			bullet.update()
			if not bullet.in_screen:
				bullet = 0
		
		# Alien bullets. They are the same but more cluster-fucky.
		del_these_bullets = []		
		for ab in range(len(alien_bullets)):
			alien_bullets[ab].update()
			if not alien_bullets[ab].in_screen:
				del_these_bullets.append(ab)
		for ab in del_these_bullets:
			del alien_bullets[ab]
			
		# Bullets drop from random alien. Frequency of drops increase with score
		score_to_frequency = {0:25, 500:50, 1000:100, 2000:800, 4000:1600, 8000:3200, 16000:6400}
		stf = 0
		for s in score_to_frequency.keys():
			if score >= s and score_to_frequency[s] > stf:
				stf = score_to_frequency[s]
		if random.randint(0, 10000) <= stf:
			random_alien_column = random.choice(aliens)
			if random_alien_column:
				alien_bullet_spawn = random.choice(random_alien_column).get_rect()
				alien_bullets.append( Alien_Bullet(alien_bullet_spawn.centerx, alien_bullet_spawn.centery) )
		
		# Handles aliens' movements downward.		
		if get_alien_boundaries("left", aliens) <= 0:
			for row in aliens:
				for a in row:
					a.switch_direction("right")
					a.move_down_row()
		elif get_alien_boundaries("right", aliens) >= screen_width:
			for row in aliens:
				for a in row:
					a.switch_direction("left")
					a.move_down_row()
					
		# Moves aliens left or right. Also checks if bullets have collided with them and destroys them adding to player's score
		for row in aliens:
			deadalien = False
			for i in range(0, len(row)):
				a = row[i]
				a.update()
				if bullet and a.get_rect().colliderect( bullet.get_rect() ):
					bullet = 0
					deadalien = i
					score += 10
			if deadalien is not False:
				del row[i]
		
		# If you kill all the aliens, there are always more :)
		if check_if_aliens_dead(aliens):
			aliens = setup_aliens()
			
		# self explanatory		
		if check_if_gameover(aliens, player, alien_bullets):
			return score
			

"""#########################################################
   #                   BEGIN MAIN                          #
   #########################################################
"""			

while __name__ == "__main__":
	# Upate screen
	pygame.display.update()
	display_highscores()
	# Check if trying to close window
	for event in pygame.event.get(): 
		if event.type == QUIT: 
			pygame.quit()
			sys.exit()
	pressed_keys = pygame.key.get_pressed()
	# Game returns score. Check if score is in high score and add it
	if pressed_keys[K_SPACE]:
		score = game()
		if get_highscore_placement(score):
			add_to_high_score(score, get_highscore_placement(score) )
		time.sleep(1)